package api

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/disintegration/imaging"
	"image"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/dgrijalva/jwt-go"

	"github.com/labstack/echo"
	. "mlgmd.com/hearsay/base"
	"mlgmd.com/hearsay/entities"
	"mlgmd.com/hearsay/model"
	"mlgmd.com/hearsay/utils"
	"xorm.io/xorm"
)

// var DB = model.DB

var JWTKey = []byte("h98-0df+igs&i3ct5y9X2ur%e32yHw8=9d2~1w?e02*8y!c")

type UserClaims struct {
	jwt.StandardClaims
	Id    int    `json:"id"`
	Roles string `json:"roles"`
	Time  int64  `json:"time"`
}

//// Login 中间件，检查token并写入用户信息
//func Login(h echo.HandlerFunc) echo.HandlerFunc {
//	return func(c echo.Context) error {
//		token := c.Request().Header.Get("Token")
//		if token == "" {
//			token = c.QueryParam("token")
//		}
//		claims := UserClaims{}
//		_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
//			return JWTKey, nil
//		})
//		if err != nil {
//			log.Println("token", token, claims)
//			return c.JSON(http.StatusUnauthorized, "用户未登录")
//		}
//		now := time.Now().Unix()
//		if claims.Time+3600*24 < now {
//			return c.JSON(http.StatusUnauthorized, "请重新登录")
//		} else if claims.Time+60*5 < now {
//			claims.Time = now
//			WriteToken(c, claims)
//		}
//		c.Set("user", claims)
//		return h(c)
//	}
//}
//
//// MayLogin 中间件，尝试检查token并写入用户信息
//func MayLogin(h echo.HandlerFunc) echo.HandlerFunc {
//	return func(c echo.Context) error {
//		token := c.Request().Header.Get("Token")
//		claims := UserClaims{}
//		_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
//			return JWTKey, nil
//		})
//		now := time.Now().Unix()
//		if err != nil || claims.Time+3600*24 < now {
//			claims = UserClaims{}
//		} else if claims.Time+60*5 < now {
//			claims.Time = now
//			WriteToken(c, claims)
//		}
//		c.Set("user", claims)
//		return h(c)
//	}
//}
//
//// Auth 返回一个中间件，检查token和权限并写入用户信息
//func Auth(role string) func(h echo.HandlerFunc) echo.HandlerFunc {
//	return func(h echo.HandlerFunc) echo.HandlerFunc {
//		return func(c echo.Context) error {
//			token := c.Request().Header.Get("Token")
//			claims := UserClaims{}
//			_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
//				return JWTKey, nil
//			})
//			if err != nil {
//				log.Println("token", token, claims)
//				return c.JSON(http.StatusUnauthorized, "用户未登录")
//			}
//			now := time.Now().Unix()
//			if claims.Time+3600*24 < now {
//				return c.JSON(http.StatusUnauthorized, "请重新登录")
//			} else if claims.Time+60*5 < now {
//				claims.Time = now
//				WriteToken(c, claims)
//			}
//			if !strings.Contains(claims.Roles, role) {
//				return c.JSON(http.StatusUnauthorized, "没有访问权限")
//			}
//			c.Set("user", claims)
//			return h(c)
//		}
//	}
//}

func UserId(ctx echo.Context) int {
	u, ok := ctx.Get("user").(UserClaims)
	if !ok {
		token := ctx.Request().Header.Get("Token")
		if token == "" {
			token = ctx.QueryParam("token")
		}
		if token == "" {
			return 0
		}
		claims := UserClaims{}
		_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
			return JWTKey, nil
		})
		if err != nil {
			log.Println("无法解析 token", token, claims, err)
			//return ctx.JSON(http.StatusUnauthorized, "用户未登录")
			return 0
		}
		now := time.Now().Unix()
		//if claims.Time+3600*24 < now {
		//	//return c.JSON(http.StatusUnauthorized, "请重新登录")
		//	return 0
		//} else
		if claims.Time+60*5 < now {
			claims.Time = now
			token := Must2(jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(JWTKey))
			ctx.Response().Header().Set("Access-Control-Expose-Headers", "X-Set-Token")
			ctx.Response().Header().Set("X-Set-Token", token)
			ctx.Response().Header().Set("Cache-Control", "no-store") // 禁止缓存这个请求，防止账号混乱
		}
		ctx.Set("user", claims)
		u = claims
	}
	return u.Id
}

func Bind[T any](ctx echo.Context, v T) T {
	if err := ctx.Bind(&v); err != nil {
		log.Println(err)
		panic(Wrap(ErrBadParam, "解析参数失败"))
	}
	return v
}

func Ok(ctx echo.Context, msg ...string) error {
	msg = append(msg, "操作成功")
	return ctx.JSON(http.StatusOK, msg[0])
}

func Data(ctx echo.Context, data any) error {
	return ctx.JSON(http.StatusOK, data)
}

func NoCacheData(ctx echo.Context, data any) error {
	ctx.Response().Header().Set("Cache-Control", "no-store") // 禁止缓存这个请求，防止账号混乱
	return ctx.JSON(http.StatusOK, data)
}

func GetPresetAvatarList() []string {
	return utils.MapAndFilter(Must2(os.ReadDir(WorkDir+"static/avatar")), func(v os.DirEntry) (string, bool) {
		return "avatar/" + v.Name(), !v.IsDir()
	})
}

//var listeners = map[int]func(event string, data string) error

var listeners = &sync.Map{} // key int 用户id value func(event string, data string) error{}

func UserListener(uid int) (func(event string, data string) error, error) {
	listener, has := listeners.Load(uid) //listeners[uid]
	if !has {
		return nil, fmt.Errorf("用户不在线")
	}
	return listener.(func(event string, data string) error), nil
}

// Send 发送消息并推送
func Send(from, to int, content string, referType string, referId int, push bool) (err error) {
	defer Error(&err)
	Must2(DB.Insert(&entities.Message{
		CreatedTime: time.Now().Unix(),
		FromUserId:  from,
		ToUserId:    to,
		Content:     content,
		ReferType:   referType,
		ReferId:     referId,
	}))
	if push {
		log.Println("推送消息:", to, content)
		if lis, err := UserListener(to); err == nil {
			go func() {
				if err := lis("message", content); err != nil {
					log.Println("推送消息提醒失败")
				}
				log.Println("推送消息提醒成功")
			}()
		} else {
			log.Println("获取用户连接失败")
		}
	}
	return
}

// Activity 用户发帖或回复时触发,发放打卡奖励
func Activity(DB xorm.Interface, uid int, typ string, id int) (err error) {
	defer Error(&err)
	now := time.Now()
	year, month, day := now.Date()
	today := time.Date(year, month, day, 0, 0, 0, 0, now.Location())
	if !Must2(DB.Table(&entities.Activity{}).Where("user_id=? and created_time>?", uid, today.Unix()).Exist()) {
		// 发放打卡奖励
		data := entities.Activity{
			UserId:      uid,
			CreatedTime: time.Now().Unix(),
			TargetType:  typ,
			TargetId:    id,
			Coin:        Config.ActivityCoin,
		}
		Must2(DB.Insert(&data))
		account := entities.Account{}
		Check2(DB.Where("user_id=?", uid).Desc("id").Get(&account))
		Must2(DB.Insert(&entities.Account{
			Type:          "sign-in",
			UserId:        uid,
			Coin:          Config.ActivityCoin,
			Weight:        Config.ActivityCoin,
			SurplusCoin:   account.SurplusCoin + Config.ActivityCoin,
			SurplusWeight: account.SurplusWeight + Config.ActivityCoin,
			ReferType:     "activity",
			ReferId:       data.Id,
			Abstract:      "打卡",
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(uid).
			SetExpr("coin", account.SurplusCoin+Config.ActivityCoin).
			SetExpr("weight", account.SurplusWeight+Config.ActivityCoin).
			Update(&entities.User{}))
		_ = Send(uid, uid, fmt.Sprintf("今日参与互动，奖励1铜板"), "", 0, true)
	}
	return
}

// ReferreeRegister 被推荐人注册
func ReferreeRegister(DB xorm.Interface, uid int, nickname string, rid int) {
	if rid == 0 {
		return
	}
	if !Must2(DB.Table(&entities.Reference{}).Where("referree_id=? and milestone = 'register'", uid).Exist()) {
		data := entities.Reference{
			ReferrerId:  rid,
			ReferreeId:  uid,
			CreatedTime: time.Now().Unix(),
			Milestone:   "register",
			Coin:        Config.ReferreeRegisterCoin,
		}
		Must2(DB.Insert(&data))
		account := entities.Account{}
		Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
		Must2(DB.Insert(&entities.Account{
			Type:          "reference",
			UserId:        rid,
			Coin:          Config.ReferreeRegisterCoin,
			Weight:        Config.ReferreeRegisterCoin,
			SurplusCoin:   account.SurplusCoin + Config.ReferreeRegisterCoin,
			SurplusWeight: account.SurplusWeight + Config.ReferreeRegisterCoin,
			ReferType:     "reference",
			ReferId:       data.Id,
			Abstract:      "推荐用户(" + nickname + ")-注册奖励",
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(rid).
			SetExpr("coin", account.SurplusCoin+Config.ReferreeRegisterCoin).
			SetExpr("weight", account.SurplusWeight+Config.ReferreeRegisterCoin).
			Update(&entities.User{}))
	}
}

// ReferreeAccount 被推荐人修改账号
func ReferreeAccount(DB xorm.Interface, uid int, nickname string, rid int) {
	if rid == 0 {
		return
	}
	if !Must2(DB.Table(&entities.Reference{}).Where("referree_id=? and milestone = 'account'", uid).Exist()) {
		data := entities.Reference{
			ReferrerId:  rid,
			ReferreeId:  uid,
			CreatedTime: time.Now().Unix(),
			Milestone:   "account",
			Coin:        Config.ReferreeActionCoin,
		}
		Must2(DB.Insert(&data))
		account := entities.Account{}
		Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
		Must2(DB.Insert(&entities.Account{
			Type:          "reference",
			UserId:        rid,
			Coin:          Config.ReferreeActionCoin,
			Weight:        Config.ReferreeActionCoin,
			SurplusCoin:   account.SurplusCoin + Config.ReferreeActionCoin,
			SurplusWeight: account.SurplusWeight + Config.ReferreeActionCoin,
			ReferType:     "reference",
			ReferId:       data.Id,
			Abstract:      "推荐用户(" + nickname + ")-首次修改用户账号奖励",
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(rid).
			SetExpr("coin", account.SurplusCoin+Config.ReferreeActionCoin).
			SetExpr("weight", account.SurplusWeight+Config.ReferreeActionCoin).
			Update(&entities.User{}))
	}
}

// ReferreeProfile 被推荐人修改资料
func ReferreeProfile(DB xorm.Interface, uid int, oldNickname, newNickname string, rid int) {
	if rid == 0 {
		return
	}
	if !Must2(DB.Table(&entities.Reference{}).Where("referree_id=? and milestone = 'profile'", uid).Exist()) {
		data := entities.Reference{
			ReferrerId:  rid,
			ReferreeId:  uid,
			CreatedTime: time.Now().Unix(),
			Milestone:   "profile",
			Coin:        Config.ReferreeActionCoin,
		}
		Must2(DB.Insert(&data))
		account := entities.Account{}
		Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
		Must2(DB.Insert(&entities.Account{
			Type:          "reference",
			UserId:        rid,
			Coin:          Config.ReferreeActionCoin,
			Weight:        Config.ReferreeActionCoin,
			SurplusCoin:   account.SurplusCoin + Config.ReferreeActionCoin,
			SurplusWeight: account.SurplusWeight + Config.ReferreeActionCoin,
			ReferType:     "reference",
			ReferId:       data.Id,
			Abstract:      "推荐用户(" + oldNickname + "->" + newNickname + ")-首次修改用户资料奖励",
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(rid).
			SetExpr("coin", account.SurplusCoin+Config.ReferreeActionCoin).
			SetExpr("weight", account.SurplusWeight+Config.ReferreeActionCoin).
			Update(&entities.User{}))
	}
}

// ReferreeInstall 被推荐人下载并打开
//func ReferreeInstall(DB xorm.Interface, uid int, oldNickname, newNickname string, rid int) {
//	if rid == 0 {
//		return
//	}
//	if !Must2(DB.Table(&entities.Reference{}).Where("referree_id=? and milestone = 'install'", uid).Exist()) {
//		data := entities.Reference{
//			ReferrerId:  rid,
//			ReferreeId:  uid,
//			CreatedTime: time.Now().Unix(),
//			Milestone:   "install",
//			Coin:        1,
//		}
//		Must2(DB.Insert(&data))
//		account := entities.Account{}
//		Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
//		Must2(DB.Insert(&entities.Account{
//			Type:          "reference",
//			UserId:        rid,
//			Coin:          1,
//			Weight:        1,
//			SurplusCoin:   account.SurplusCoin + 1,
//			SurplusWeight: account.SurplusWeight + 1,
//			ReferType:     "reference",
//			ReferId:       data.Id,
//			Abstract:      "推荐用户(" + oldNickname + "->" + newNickname + ")-首次下载安装并打开",
//			CreatedTime:   time.Now().Unix(),
//		}))
//		Must2(DB.ID(rid).
//			SetExpr("coin", account.SurplusCoin+1).
//			SetExpr("weight", account.SurplusWeight+1).
//			Update(&entities.User{}))
//	}
//}

// ReferreeActivity 被推荐人活跃
func ReferreeActivity(DB xorm.Interface, uid int, nickname string, rid int) {
	if rid == 0 {
		return
	}
	rec := make([]entities.Reference, 0)
	Must(DB.Where("referree_id=?", uid).Find(&rec))
	milestones := map[string]entities.Reference{}
	for _, ref := range rec {
		milestones[ref.Milestone] = ref
	}
	do := func(milestone string) {
		data := entities.Reference{
			ReferrerId:  rid,
			ReferreeId:  uid,
			CreatedTime: time.Now().Unix(),
			Milestone:   milestone,
			Coin:        Config.ReferreeActionCoin,
		}
		Must2(DB.Insert(&data))
		account := entities.Account{}
		Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
		Must2(DB.Insert(&entities.Account{
			Type:          "reference",
			UserId:        rid,
			Coin:          Config.ReferreeActionCoin,
			Weight:        Config.ReferreeActionCoin,
			SurplusCoin:   account.SurplusCoin + Config.ReferreeActionCoin,
			SurplusWeight: account.SurplusWeight + Config.ReferreeActionCoin,
			ReferType:     "reference",
			ReferId:       data.Id,
			Abstract:      "推荐用户(" + nickname + ")-打卡奖励",
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(rid).
			SetExpr("coin", account.SurplusCoin+Config.ReferreeActionCoin).
			SetExpr("weight", account.SurplusWeight+Config.ReferreeActionCoin).
			Update(&entities.User{}))
	}
	if _, has := milestones["activity-1"]; !has {
		do("activity-1")
		return
	}
	if _, has := milestones["activity-2"]; !has {
		do("activity-2")
		return
	}
	if _, has := milestones["activity-3"]; !has {
		do("activity-3")
		return
	}
	if _, has := milestones["activity-4"]; !has {
		do("activity-4")
		return
	}
	if _, has := milestones["activity-5"]; !has {
		do("activity-5")
		return
	}
	if _, has := milestones["activity-6"]; !has {
		do("activity-6")
		return
	}
	if _, has := milestones["activity-7"]; !has {
		do("activity-7")
		return
	}
}

// ReferreeFavored 被推荐人收到打赏
func ReferreeFavored(DB xorm.Interface, uid int, nickname string, rid int) {
	if rid == 0 {
		return
	}
	sum := 0
	Check2(DB.Table(&entities.Account{}).Where("user_id=? and type = 'hearsay-favor'", uid).Select("sum(coin)").Get(&sum))
	if !Must2(DB.Table(&entities.Reference{}).Where("referree_id=? and milestone = 'favor-100'", uid).Exist()) {
		if sum >= 100 {
			data := entities.Reference{
				ReferrerId:  rid,
				ReferreeId:  uid,
				CreatedTime: time.Now().Unix(),
				Milestone:   "favor-100",
				Coin:        100,
			}
			Must2(DB.Insert(&data))
			account := entities.Account{}
			Check2(DB.Where("user_id=?", rid).Desc("id").Get(&account))
			Must2(DB.Insert(&entities.Account{
				Type:          "reference",
				UserId:        rid,
				Coin:          100,
				Weight:        100,
				SurplusCoin:   account.SurplusCoin + 100,
				SurplusWeight: account.SurplusWeight + 100,
				ReferType:     "reference",
				ReferId:       data.Id,
				Abstract:      "推荐用户(" + nickname + ")-获得打赏超过100铜板奖励",
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(rid).
				SetExpr("coin", account.SurplusCoin+100).
				SetExpr("weight", account.SurplusWeight+100).
				Update(&entities.User{}))
		}
	}
}

//func Freeze() (err error) {
//	defer Error(&err)
//	log.Println("Freeze start")
//	if Must2(DB.Table(&entities.TaskRecord{}).Where("start_time > ?", time.Now().Unix()-24*60*60-1000).Exist()) {
//		log.Println("最近已执行过长时间未处理已解锁的传闻冻结铜板的任务")
//		return fmt.Errorf("最近已执行过")
//	}
//	rec := entities.TaskRecord{
//		StartTime: time.Now().Unix(),
//	}
//	Must2(DB.Insert(&rec))
//
//	Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
//		log.Println("Freeze do")
//		unlocks := make([]entities.HearsayUnlock, 0)
//		Must(DB.Table([]any{&entities.Hearsay{}, "h"}).
//			Join("INNER", []any{&entities.HearsayUnlock{}, "hu"}, "hu.hearsay_id=h.id").
//			Where("h.deleted_time = 0 and h.blocked = 0").
//			Where("hu.created_time < ? and hu.result=0", time.Now().Unix()-24*60*60).Find(&unlocks))
//		users := make([]entities.User, 0)
//		Must(DB.In("id", utils.KeyOfSlice(unlocks, "user")).Find(&users))
//		um := map[int]*struct {
//			Coin   int
//			Frozen int
//		}{}
//		for _, user := range users {
//			um[user.Id] = &struct {
//				Coin   int
//				Frozen int
//			}{Coin: user.Coin, Frozen: 0}
//		}
//		for _, unlock := range unlocks {
//			if u, has := um[unlock.UserId]; has && u.Coin > u.Frozen {
//				u.Frozen++
//				Must2(DB.ID(unlock.Id).Incr("frozen_coin").Update(&entities.HearsayUnlock{}))
//			}
//		}
//		for id, u := range um {
//			if u.Frozen > 0 {
//				Must2(DB.ID(id).Decr("coin", u.Frozen).Update(&entities.User{}))
//				_ = Send(id, id, fmt.Sprintf("你有%d个铜板被暂时冻结，原因是有已解锁的传闻未点赞或点踩，请及时处理。", u.Frozen), "", 0, true)
//			}
//		}
//		Must2(DB.Table(&entities.TaskRecord{}).ID(rec.Id).Update(map[string]any{
//			"finish_time": time.Now().Unix(),
//			"result":      "completed",
//		}))
//		return nil, nil
//	}))
//	log.Println("Freeze completed")
//	return nil
//}

const Version = "0.9.0"

// CheckVersion 检查版本是否需要升级
func CheckVersion(v string) bool {
	if v == "" {
		return true
	}
	current := strings.Split(Version, ".")
	client := strings.Split(v, ".")
	// 逐个比较版本号的每个部分
	for index, part := range current {
		if len(client) <= index {
			return true
		}
		cp, _ := strconv.Atoi(client[index])
		p, _ := strconv.Atoi(part)
		if cp < p {
			return true
		}
		if cp > p {
			// 如果客户端的版本号比较大，则不需要更新，但是一般不会出现这种情况
			return false
		}
	}
	return false
}

// Route 命名规则:[/module][/role][/object][/params...][/action]
func Route(e *echo.Echo) {
	e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) (err error) {
			defer func() {
				if info := recover(); info != nil {
					if _err, ok := info.(error); ok {
						if errors.Is(_err, ErrBadParam) {
							err = c.JSON(http.StatusBadRequest, _err.Error())
							return
						}
						if errors.Is(_err, ErrFinished) {
							err = c.JSON(http.StatusForbidden, _err.Error())
							return
						}
						if errors.Is(_err, ErrNotFound) {
							err = c.JSON(http.StatusNotFound, _err.Error())
							return
						}
						if errors.Is(_err, ErrForbidden) {
							err = c.JSON(http.StatusForbidden, _err.Error())
							return
						}
						if errors.Is(_err, ErrInternal) {
							err = c.JSON(http.StatusInternalServerError, _err.Error())
							return
						}
					}
					if _err, ok := info.(LocatedError); ok {
						log.Println("已定位的错误:", _err.String())
					} else {
						log.Println("未捕获的异常:", info)
						debug.PrintStack()
					}
					err = c.JSON(http.StatusInternalServerError, "服务内部错误，请及时联系管理员处理")
				}
			}()
			return h(c)
		}
	})
	/*
	 * 以下路径用来输出静态文件
	 */
	e.Static("/", "static")
	e.GET("/manifest.json", func(ctx echo.Context) error {
		return ctx.JSON(http.StatusOK, map[string]any{
			"name": "江湖传闻",
			"icons": []any{
				map[string]any{
					"src": "icons/512.png",
					//"src":   "",
					"type":  "image/png",
					"sizes": "200x200",
				},
			},
			"start_url": ".",
			"display":   "standalone",
			//"screenshots": []any{
			//	map[string]any{
			//		"src":   "screenshots/1.png",
			//		"sizes": "3840x2160",
			//		"type":  "image/png",
			//	},
			//},
		})
	})
	e.GET("/install", func(ctx echo.Context) error {
		return ctx.Attachment("static/index.html", "江湖传闻.html")
	})

	/*
	 * 以下路径用来处理用户上传文件
	 */
	e.POST("/upload", func(c echo.Context) error {
		file := Must2(c.FormFile("file"))
		data := Must2(io.ReadAll(Must2(file.Open())))
		code := md5.Sum(data)
		ps := strings.Split(file.Filename, ".")
		name := hex.EncodeToString(code[:]) + "." + ps[len(ps)-1]
		Must(os.WriteFile(WorkDir+"upload/"+name, data, 0666))

		type Result struct {
			Ok  bool   `json:"ok"`
			Msg string `json:"msg"`
			Url string `json:"url"`
		}
		return c.JSON(http.StatusOK, Result{
			Ok:  true,
			Msg: "上传成功",
			Url: name,
		})
	})
	e.GET("/download/:path", func(ctx echo.Context) (err error) {
		path := Check2(url.PathUnescape(ctx.Param("path"))).BadParam("文件路径错误")
		avatar := ctx.QueryParam("avatar") != "" // 如果是头像时可以压缩图片的像素
		// 返回压缩过的图片
		uploadPath := WorkDir + "upload/" + path
		tempPath := WorkDir + "temp/" + path
		if avatar {
			tempPath = WorkDir + "temp/avatar-" + path
		}
		if _, err = os.Stat(tempPath); err != nil {
			source, err := os.Open(uploadPath)
			if err != nil {
				return ctx.File(uploadPath)
			}
			defer source.Close()
			img, format, err := image.Decode(source)
			if err != nil {
				panic(err)
			}
			switch format {
			case "jpg", "jpeg", "png":
			default:
				return ctx.File(uploadPath)
			}
			if avatar {
				img = imaging.Resize(img, 128, 128, imaging.Lanczos)
			}
			Must(imaging.Save(img, tempPath))
			//temp, err := os.Create(tempPath)
			//if err != nil {
			//	panic(err)
			//}
			//defer temp.Close()
			//err = webp.Encode(temp, img, Must2(encoder.NewLossyEncoderOptions(encoder.PresetDefault, 90)))
			//if err != nil {
			//	panic(err)
			//}
			return ctx.File(tempPath)
		}
		return ctx.File(tempPath)
		//defer func() {
		//	if info := recover(); info != nil {
		//		log.Println("添加水印失败", info)
		//		err = fmt.Errorf("添加水印失败")
		//	}
		//}()
		//probe := entities.Probe{}
		//Must2(DB.Where("user_id=?", UserId(ctx)).Decr("created_time").Get(&probe))
		//utils.Draw(WorkDir+"upload/"+name, probe.OnceCode, ctx.Response())
		//return err
	})

	// 检查服务是否能正常响应
	e.Any("/ping", func(ctx echo.Context) error {
		return Ok(ctx)
	})
	// 追踪用户的访问
	e.POST("/track", func(ctx echo.Context) error {
		params := Bind(ctx, entities.TrackItem{})
		params.Id = 0
		params.Ip = ctx.RealIP()
		params.UserAgent = ctx.Request().UserAgent()
		Must2(DB.Insert(&params))
		return Ok(ctx)
	})
	// 客户端发送设备信息、token，服务端返回当前状态、更新token、是否需要登录
	e.POST("/probe", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Probe{})
		// 如果没有设备码，则生成
		if params.DeviceCode == "" {
			chars := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
			code := []rune("")
			for i := 0; i < 20; i++ {
				code = append(code, chars[rand.Intn(len(chars))])
			}
			params.DeviceCode = string(code)
		}
		{
			chars := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
			code := []rune("")
			for i := 0; i < 8; i++ {
				code = append(code, chars[rand.Intn(len(chars))])
			}
			params.OnceCode = string(code)
		}

		return Data(ctx, Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			// 如果没有用户，则创建
			user := entities.User{}
			if UserId(ctx) != 0 {
				Must2(DB.ID(UserId(ctx)).Get(&user))
			}
			if user.Id == 0 {
				// 创建游客用户
			gen:
				username := strconv.Itoa(rand.Intn(1000000))
				if Must2(DB.Table(&entities.User{}).Where("username=?", username).Exist()) {
					goto gen
				}
				avatars := GetPresetAvatarList()
				user = entities.User{
					Username:   username,
					DeviceCode: params.DeviceCode,
					UserProfile: entities.UserProfile{
						Avatar:    avatars[rand.Intn(len(avatars))],
						Nickname:  "游客" + username,
						Signature: "这个用户很懒，还没修改用户信息",
						Gender:    0,
						Publicly:  false,
					},
					CreatedTime: time.Now().Unix(),
				}
				if params.ReferrerId != 0 {
					if Must2(DB.Table(&entities.Probe{}).Where("device_code=?", params.DeviceCode).Exist()) {
						user.ReferrerError = "设备被注册过，ref:" + strconv.Itoa(params.ReferrerId)
					} else if Must2(DB.Table(&entities.Probe{}).Where("ip = ? and user_agent=?", ctx.RealIP(), ctx.Request().UserAgent()).Exist()) {
						user.ReferrerError = "设备疑似被注册过，ref:" + strconv.Itoa(params.ReferrerId)
					} else {
						user.ReferrerId = params.ReferrerId
					}
				}
				Must2(DB.Insert(&user))
				ReferreeRegister(DB, user.Id, user.Nickname, user.ReferrerId)
				token := Must2(jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{
					Id:    user.Id,
					Roles: user.Roles,
					Time:  time.Now().Unix(),
				}).SignedString(JWTKey))
				ctx.Response().Header().Set("Access-Control-Expose-Headers", "X-Set-Token")
				ctx.Response().Header().Set("X-Set-Token", token)
			}
			Must2(DB.Insert(&entities.Probe{
				UserId:      user.Id,
				DeviceCode:  params.DeviceCode,
				OnceCode:    params.OnceCode,
				Version:     params.Version,
				ReferrerId:  params.ReferrerId,
				Origin:      ctx.Request().Header.Get("origin"),
				Endpoint:    ctx.Request().Host,
				Ip:          ctx.RealIP(),
				UserAgent:   ctx.Request().UserAgent(),
				CreatedTime: time.Now().Unix(),
			}))
			log.Println("RemoteAddr", ctx.Request().RemoteAddr)
			notice, to := "", ""
			if CheckVersion(params.Version) {
				notice = "有新版本可以使用啦！点击跳转到新版本"
				to = func() string {
					if ctx.IsTLS() {
						return "https://"
					} else {
						return "http://"
					}
				}() + ctx.Request().Host
				//to = "https://hs.mlgmd.com"
			}
			return map[string]any{
				"config":     Config.DynamicConfig,
				"user":       user,
				"deviceCode": params.DeviceCode,
				"onceCode":   params.OnceCode,
				"notice":     notice,
				"url":        to,
				"redirect":   false, // 收到后立即跳转
			}, nil
		})))
	})
	//e.POST("/account/register", func(ctx echo.Context) error {
	//	params := Bind(ctx, entities.User{})
	//	When(params.Username == "").BadParam("用户名不能为空")
	//	When(params.Password == "").BadParam("密码不能为空")
	//
	//	Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
	//		When(Must2(DB.Table(&entities.User{}).Where("username =?", params.Username).Exist())).BadParam("用户名已被占用")
	//		slat := strconv.Itoa(rand.Intn(900000) + 100000)
	//		code := md5.Sum([]byte(params.Password + slat))
	//		user := entities.User{
	//			Username: params.Username,
	//			Password: hex.EncodeToString(code[:]),
	//			Salt:     slat,
	//		}
	//		Check2(DB.Insert(&user)).Cover("创建用户失败")
	//		Must2(DB.Insert(&entities.Probe{
	//			UserId:      user.Id,
	//			DeviceCode:  params.DeviceCode,
	//			Endpoint:    ctx.Request().Host,
	//			Ip:          ctx.RealIP(),
	//			UserAgent:   ctx.Request().UserAgent(),
	//			CreatedTime: time.Now().Unix(),
	//		}))
	//		return nil, nil
	//	}))
	//	return ctx.JSON(http.StatusOK, "注册成功")
	//})
	e.POST("/account/login", func(ctx echo.Context) error {
		params := Bind(ctx, entities.User{}) // username password deviceCode
		When(params.Username == "").BadParam("用户名不能为空")
		When(params.Password == "").BadParam("密码不能为空")

		user := entities.User{}
		Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			When(!Must2(DB.Where("username =?", params.Username).Get(&user))).BadParam("用户名不存在")
			code := md5.Sum([]byte(params.Password + user.Salt))
			When(hex.EncodeToString(code[:]) != user.Password).BadParam("用户名或密码错误")
			Must2(DB.Insert(&entities.Probe{
				UserId:      user.Id,
				DeviceCode:  params.DeviceCode,
				Endpoint:    ctx.Request().Host,
				Ip:          ctx.RealIP(),
				UserAgent:   ctx.Request().UserAgent(),
				CreatedTime: time.Now().Unix(),
			}))
			return nil, nil
		}))

		token := Must2(jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{
			Id:    user.Id,
			Roles: user.Roles,
			Time:  time.Now().Unix(),
		}).SignedString(JWTKey))
		ctx.Response().Header().Set("Access-Control-Expose-Headers", "X-Set-Token")
		ctx.Response().Header().Set("X-Set-Token", token)
		return Data(ctx, map[string]any{
			"message": "登录成功",
			"data":    user,
		})
	})
	//e.GET("/account/get", func(ctx echo.Context) error {
	//	user := entities.User{}
	//	When(!Must2(DB.ID(UserId(ctx)).Get(&user))).BadParam("账号不存在")
	//	return Data(ctx, user)
	//})
	e.POST("/account/set", func(ctx echo.Context) error {
		params := Bind(ctx, entities.User{})
		When(params.Username == "").BadParam("用户名不能为空")
		When(params.Password == "").BadParam("密码不能为空")

		Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			user := entities.User{}
			When(!Must2(DB.ID(UserId(ctx)).Get(&user))).BadParam("账号不存在")
			if user.Username != params.Username {
				When(Must2(DB.Table(&entities.User{}).Where("username =?", params.Username).Exist())).
					BadParam("用户名已被占用")
			}
			slat := strconv.Itoa(rand.Intn(900000) + 100000)
			code := md5.Sum([]byte(params.Password + slat))
			Check2(DB.ID(UserId(ctx)).Update(&entities.User{
				Username: params.Username,
				Password: hex.EncodeToString(code[:]),
				Salt:     slat,
				Contact:  params.Contact,
			})).Internal("修改账号失败")
			ReferreeAccount(DB, user.Id, user.Nickname, user.ReferrerId)
			return nil, nil
		}))
		return Ok(ctx)
	})
	// 补充联系人用的接口
	e.POST("/account/contact", func(ctx echo.Context) error {
		params := Bind(ctx, entities.User{})
		Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			user := entities.User{}
			When(!Must2(DB.ID(UserId(ctx)).Get(&user))).BadParam("账号不存在")
			Check2(DB.ID(UserId(ctx)).Update(&entities.User{
				Contact: params.Contact,
			})).Internal("修改账号失败")
			return nil, nil
		}))
		return Ok(ctx)
	})

	e.GET("/events", func(ctx echo.Context) error {
		header := ctx.Response().Header()
		header.Set("Content-Type", "text/event-stream")
		header.Set("Cache-Control", "no-cache")
		header.Set("Connection", "keep-alive")
		header.Set("Access-Control-Allow-Origin", "*")
		listeners.Store(UserId(ctx), func(event string, data string) error {
			if _, err := ctx.Response().Write([]byte("event:" + event + "\ndata:" + data + "\n\n")); err != nil {
				listeners.Delete(UserId(ctx))
				log.Println("事件发送失败", err)
			}
			ctx.Response().Flush()
			return nil
		})
		if _, err := ctx.Response().Write([]byte("event:conn\ndata:ok\n\n")); err != nil {
			listeners.Delete(UserId(ctx))
			log.Println("事件发送失败", err)
		}
		ctx.Response().Flush()
		<-ctx.Response().CloseNotify()
		listeners.Delete(UserId(ctx))
		return nil
	})
	e.GET("/message", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]model.Message, 0)
		query := Query(DB).Where("m.to_user_id = ?", UserId(ctx))
		if params.Keyword != "" {
			query.Where("content like ?", "%"+params.Keyword+"%")
		}
		count := Must2(query.Desc("m.created_time").Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return NoCacheData(ctx, params.Result(data, count))
	})
	e.GET("/accounts", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]entities.Account, 0)
		query := DB.Where("user_id=?", UserId(ctx))
		if params.Keyword != "" {
			query.Where("abstract like ?", "%"+params.Keyword+"%")
		}
		count := Must2(query.Desc("created_time").Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return NoCacheData(ctx, params.Result(data, count))
	})
	e.POST("/transfer", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Transfer{})
		When(params.ToUserId == UserId(ctx)).BadParam("请不要给自己赠送铜板")
		When(params.Coin <= 0).BadParam("赠送铜板数量不能小于1")

		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			from := entities.User{}
			When(!Must2(DB.ID(UserId(ctx)).Get(&from))).NotFound("用户不存在")
			When(from.Blocked).Forbidden("您的账号已被禁用")
			fromAccount := entities.Account{}
			Check2(DB.Where("user_id=?", from.Id).Desc("id").Get(&fromAccount))
			When(params.Coin > fromAccount.SurplusCoin).BadParam("您的铜板不足")
			to := entities.User{}
			When(!Must2(DB.ID(params.ToUserId).Get(&to))).NotFound("用户不存在")
			toAccount := entities.Account{}
			Check2(DB.Where("user_id=?", to.Id).Desc("id").Get(&toAccount))

			coin := params.Coin - params.Coin/5 // 实收的铜板
			data := entities.Transfer{
				CreatorId:   from.Id,
				CreatedTime: time.Now().Unix(),
				ToUserId:    to.Id,
				Content:     params.Content,
				Coin:        params.Coin,
			}
			Must2(DB.Insert(&data))
			Must2(DB.Insert(&entities.Account{
				Type:          "transfer-out",
				UserId:        from.Id,
				Coin:          -params.Coin,
				Weight:        -params.Coin,
				SurplusCoin:   fromAccount.SurplusCoin - params.Coin,
				SurplusWeight: fromAccount.SurplusWeight - params.Coin,
				ReferType:     "transfer",
				ReferId:       data.Id,
				Abstract:      utils.CuteString(fmt.Sprintf("你赠送给%s %d铜板:%s", to.Nickname, params.Coin, params.Content), 40),
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(from.Id).
				SetExpr("coin", fromAccount.SurplusCoin-params.Coin).
				SetExpr("weight", fromAccount.SurplusWeight-params.Coin).
				Update(&from))
			Must2(DB.Insert(&entities.Account{
				Type:          "transfer-in",
				UserId:        to.Id,
				Coin:          coin,
				Weight:        coin,
				SurplusCoin:   toAccount.SurplusCoin + coin,
				SurplusWeight: toAccount.SurplusWeight + coin,
				ReferType:     "transfer",
				ReferId:       data.Id,
				Abstract:      utils.CuteString(fmt.Sprintf("%s赠送你%d铜板:%s(实收%d)", from.Nickname, params.Coin, params.Content, coin), 40),
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(to.Id).
				SetExpr("coin", toAccount.SurplusCoin+coin).
				SetExpr("weight", toAccount.SurplusWeight+coin).
				Update(&to))

			_ = Send(from.Id, to.Id, fmt.Sprintf("%s赠送你%d铜板:%s", from.Nickname, params.Coin, params.Content), "", 0, true)
			_ = Send(to.Id, from.Id, fmt.Sprintf("你赠送给%s %d铜板:%s", to.Nickname, params.Coin, params.Content), "", 0, false)
			return
		}))
		return Ok(ctx)
	})

	e.GET("/person", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})

		data := make([]entities.User, 0)
		query := Query(DB).Where("blocked = 0 and publicly = 1")
		if params.Keyword != "" {
			query.Where("nickname like ? or signature like ?", utils.Repeat(any("%"+params.Keyword+"%"), 2)...)
			query.Or("username = ?", params.Keyword) // 当关键词与用户名匹配时会忽略前面的条件
		}
		if params.Sort == "latest" {
			query.Desc("created_time")
		} else {
			query.Desc("weight")
		}
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(utils.Foreach(data, MaskPtr[*entities.User]), count))
	})
	e.GET("/manager/person", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})

		data := make([]entities.User, 0)
		query := Query(DB)
		if params.Keyword != "" {
			query.Where("nickname like ? or signature like ?", utils.Repeat(any("%"+params.Keyword+"%"), 2)...)
			query.Or("username = ?", params.Keyword) // 当关键词与用户名匹配时会忽略前面的条件
		}
		if params.Sort == "latest" {
			query.Desc("created_time")
		} else {
			query.Desc("weight")
		}
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(utils.Foreach(data, MaskPtr[*entities.User]), count))
	})
	e.GET("/person/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		data := Must2(model.Person{}.Get(DB, id))
		data.Mask()
		return Data(ctx, data)
	})
	e.GET("/person/mine", func(ctx echo.Context) error {
		return NoCacheData(ctx, Must2(model.Person{}.Get(DB, UserId(ctx))))
	})
	e.PUT("/person/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		params := Bind(ctx, entities.UserProfile{})
		Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			user := entities.User{}
			When(!Check2(DB.ID(id).Get(&user)).Internal()).NotFound("用户不存在")
			When(user.Id != UserId(ctx)).Forbidden()

			Must2(DB.Table(&entities.User{}).Where("id=?", id).AllCols().Update(params))
			ReferreeProfile(DB, user.Id, user.Nickname, params.Nickname, user.ReferrerId)
			return nil, nil
		}))
		return Ok(ctx)
	})
	e.POST("/person-comment", func(ctx echo.Context) error {
		params := Bind(ctx, entities.UserComment{})
		When(params.Content == "").BadParam("内容不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			person := entities.User{}
			Check2(DB.ID(params.TargetId).Get(&person)).BadParam("用户不存在")
			data := params
			data.CreatorId = from.Id
			data.CreatedTime = time.Now().Unix()
			Must2(DB.Insert(&data))
			Must2(DB.ID(data.TargetId).Incr("comment_count").Update(&person))

			_ = Send(from.Id, from.Id, fmt.Sprintf("你对%s发表了评论:%s", person.Nickname, params.Content), "person", person.Id, false)
			if person.Id != from.Id {
				_ = Send(from.Id, person.Id, fmt.Sprintf("%s评论你:%s", from.Nickname, params.Content), "person", person.Id, true)
			}
			if params.ReplyToId != 0 {
				replyTo := entities.HearsayComment{}
				Check2(DB.ID(params.ReplyToId).Get(&replyTo))
				if replyTo.CreatorId != from.Id && replyTo.CreatorId != person.Id {
					_ = Send(from.Id, replyTo.CreatorId, fmt.Sprintf("%s回复你的评论:%s", from.Nickname, params.Content), "person", person.Id, true)
				}
			}
			if err := Activity(DB, UserId(ctx), "person-comment", data.Id); err != nil {
				log.Println(err)
			}
			ReferreeActivity(DB, UserId(ctx), from.Nickname, from.ReferrerId)
			return
		}))
		return Ok(ctx)
	})
	e.DELETE("/person-comment/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			data := entities.UserComment{}
			Must2(DB.ID(id).Get(&data))
			When(data.CreatorId != UserId(ctx)).Forbidden()
			Must2(DB.ID(id).SetExpr("deleted_time", time.Now().Unix()).Update(&data))
			return
		}))
		return Ok(ctx)
	})
	e.POST("/manager/person/:id/reset-password", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		data := Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			user := entities.User{}
			When(!Check2(DB.ID(id).Get(&user)).Internal()).NotFound("用户不存在")

			slat := strconv.Itoa(rand.Intn(900000) + 100000)
			password := strconv.Itoa(rand.Intn(900000) + 100000)
			code := md5.Sum([]byte(password + slat))
			Must2(DB.Table(&entities.User{}).Where("id=?", id).Update(map[string]any{
				"password": hex.EncodeToString(code[:]),
				"salt":     slat,
			}))
			return map[string]any{
				"username": user.Username,
				"password": password,
			}, nil
		}))
		return Data(ctx, data)
	})

	e.GET("/hearsay", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		filter, filterCount := false, 0 // 是否过滤掉可能看过的内容
		//user := entities.User{}
		data := make([]model.HearsayListItem, 0)
		query := model.HearsayListItem{}.Query(DB, UserId(ctx))
		if params.Keyword != "" {
			query.Where("u.nickname like ? or h.public_content like ?", utils.Repeat(any("%"+params.Keyword+"%"), 2)...)
			// 查找评论中包含关键词的传闻的id
			cis := make([]int, 0)
			Must(DB.Table(&entities.HearsayComment{}).Where("content like ?", "%"+params.Keyword+"%").
				GroupBy("target_id").Select("target_id").Find(&cis))
			if len(cis) > 0 {
				query.Or("h.id in (" + strings.Join(utils.Map(cis, strconv.Itoa), ",") + ")")
			}
		}
		if params.Sort == "browsed" {
			query.Where("hb.id is not null")
			query.Desc("h.last_comment_time")
		} else if params.Sort == "latest" {
			query.Desc("h.created_time")
		} else {
			query.Desc("h.weight")
			if Config.RecommendHearsayFilter && params.Keyword == "" && params.Seq != 0 { // 当关键词为空时过滤生效
				filter = true
				filterCount = int(Must2(DB.Where("user_id=? and seq != ? and confirmed", UserId(ctx), params.Seq).
					Count(&entities.HearsayFilter{})))
			}
		}
		query.Where("u.blocked = 0 and h.blocked = 0 and h.deleted_time = 0")
		if filter { // 启用过滤时不需要考虑偏移量
			query.Join("LEFT", []any{entities.HearsayFilter{}, "hf"},
				"hf.user_id=? and hf.hearsay_id=h.id and hf.seq != ? and hf.confirmed", UserId(ctx), params.Seq)
			query.Where("hf.id is null")
			//query.Where("crc32(h.id) != crc32(h.id) & " + strconv.Itoa(user.HearsayFilter+user.HearsayFilterNext)).Limit(params.Size())
		} else {
			//query.Limit(params.Size(), params.Offset())
		}
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		slice := func(str string, length int) string {
			runes := []rune(str)
			if len(runes) > length {
				return string(runes[:length]) + "..."
			}
			return str
		}
		for index, elem := range data {
			data[index].PublicContent = slice(elem.PublicContent, 150)
			data[index].RewardContent = slice(elem.RewardContent, 150)
			data[index].Mask()
		}
		if filter && len(data) > 0 { // 启用过滤时，记录推荐过的传闻
			go func(uid, seq int) {
				Must2(DB.Where("user_id = ? and seq = ? and !confirmed", uid, seq).
					SetExpr("confirmed", 1).Update(&entities.HearsayFilter{}))
				Must2(DB.Insert(utils.MapWithIndex(data, func(i int, v model.HearsayListItem) entities.HearsayFilter {
					return entities.HearsayFilter{
						HearsayId:   v.Id,
						UserId:      uid,
						Seq:         params.Seq,
						Confirmed:   i < 5,
						CreatedTime: time.Now().Unix(),
					}
				})))
			}(UserId(ctx), params.Seq)
			//var list, next []model.HearsayListItem
			//if len(data) > 5 {
			//	list, next = data[:5], data[len(data)-5:]
			//} else {
			//	list = data
			//}
			//s := DB.ID(UserId(ctx)).
			//	SetExpr("hearsay_filter", "hearsay_filter|hearsay_filter_next|"+strings.Join(utils.Map(list, func(v model.HearsayListItem) string {
			//		return "crc32(" + strconv.Itoa(v.Id) + ")"
			//	}), "|")).
			//	Incr("hearsay_filter_count", len(list))
			//if len(next) > 0 {
			//	s.
			//		SetExpr("hearsay_filter_next", strings.Join(utils.Map(next, func(v model.HearsayListItem) string {
			//			return "crc32(" + strconv.Itoa(v.Id) + ")"
			//		}), "|")).
			//		Incr("hearsay_filter_count_next", len(next))
			//}
			//Must2(s.Update(&entities.User{}))
		}
		return Data(ctx, struct {
			ListResult
			FilterCount int `json:"filterCount"`
		}{
			params.Result(data, count),
			filterCount,
		})
	})
	e.GET("/hearsay/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		data := Must2(model.Hearsay{}.Get(DB, id))
		data.ProtectedEmpty = data.ProtectedContent == "" && len(data.ProtectedImages) == 0

		unlocks := make([]entities.HearsayUnlock, 0)
		Must(Query(DB).Where("hearsay_id = ?", id).Find(&unlocks))
		unlockMap := utils.MapByKey(unlocks, "user")
		data.Unlock = unlockMap[UserId(ctx)]
		if data.CreatorId != UserId(ctx) && data.Unlock.CreatedTime == 0 {
			data.Mask()
			ctx.Response().Header().Set("Cache-Control", "no-store") // 禁止缓存这个请求，防止账号混乱
		}
		for i, c := range data.Comments {
			unlock := unlockMap[c.CreatorId]
			c.Unlocked = unlock.CreatedTime != 0
			c.Result = unlock.Result
			data.Comments[i] = c
		}

		if Must2(DB.Table(&entities.HearsayBrowse{}).Where("user_id=? and hearsay_id=?", UserId(ctx), id).Exist()) {
			Must2(DB.Where("user_id=? and hearsay_id=?", UserId(ctx), id).SetExpr("created_time", time.Now().Unix()).Update(&entities.HearsayBrowse{}))
		} else {
			Must2(DB.Insert(&entities.HearsayBrowse{
				HearsayId:   id,
				UserId:      UserId(ctx),
				CreatedTime: time.Now().Unix(),
			}))
			Must2(DB.ID(id).Incr("browse_count").Update(&entities.Hearsay{}))
		}
		return Data(ctx, data)
	})
	e.POST("/hearsay", func(ctx echo.Context) error {
		params := Bind(ctx, struct {
			entities.HearsayContent
			RewardId int `json:"rewardId"` // 当悬赏id不为空时，同时作为投稿
		}{})
		When(params.PublicContent == "").BadParam("内容不能为空")

		id := Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			reward := entities.Reward{}
			if params.RewardId != 0 {
				Check2(DB.ID(params.RewardId).Get(&reward)).BadParam("悬赏不存在")
			}
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			max := from.HearsayMaxCoin
			if max == 0 {
				max = Config.DefaultHearsayMaxCoin
			}
			When(params.Coin > max).BadParam("您只能发布" + strconv.Itoa(max) + "个铜板以下的传闻，要提升权限请联系管理员")
			if params.Coin < Config.HearsayMinCoin {
				params.Coin = Config.HearsayMinCoin
			}
			data := entities.Hearsay{
				CreatorId:         UserId(ctx),
				CreatedTime:       time.Now().Unix(),
				HearsayContent:    params.HearsayContent,
				RewardId:          reward.Id,
				RewardContent:     reward.Content,
				LastCommentUserId: UserId(ctx),
				LastCommentTime:   time.Now().Unix(),
			}
			Must2(DB.Insert(&data))
			Must2(DB.ID(data.CreatorId).Incr("hearsay_count").Update(&entities.User{}))
			if reward.Id != 0 {
				Must2(DB.Insert(&entities.RewardHearsay{
					RewardId:    reward.Id,
					HearsayId:   data.Id,
					UserId:      data.CreatorId,
					CreatedTime: time.Now().Unix(),
				}))
				Must2(DB.ID(reward.Id).Incr("hearsay_count").Update(&reward))

				content := []rune(data.PublicContent)
				if len(content) > 20 {
					content = content[:20]
				}
				_ = Send(from.Id, reward.CreatorId, fmt.Sprintf("%s给你的悬赏投稿:%s", from.Nickname, string(content)), "reward", reward.Id, true)
			}
			_ = Activity(DB, UserId(ctx), "hearsay", data.Id)
			ReferreeActivity(DB, UserId(ctx), from.Nickname, from.ReferrerId)
			return data.Id, nil
		}))
		return Data(ctx, id)
	})
	e.DELETE("/hearsay/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			data := entities.Hearsay{}
			When(!Must2(DB.ID(id).Get(&data))).NotFound("传闻不存在")
			When(data.CreatorId != UserId(ctx)).Forbidden()
			Must2(DB.ID(id).SetExpr("deleted_time", time.Now().Unix()).Update(&data))
			return
		}))
		return Ok(ctx)
	})
	e.POST("/hearsay/:id/unlock", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			When(from.Blocked).Forbidden("您的账号已被禁用")
			hearsay := entities.Hearsay{}
			Check2(DB.ID(id).Get(&hearsay)).BadParam("传闻不存在")
			When(from.Id == hearsay.CreatorId).BadParam("不需要解锁自己的传闻")
			to := entities.User{}
			Check2(DB.ID(hearsay.CreatorId).Get(&to)).BadParam("用户不存在")
			account := entities.Account{}
			Check2(DB.Where("user_id=?", from.Id).Desc("id").Get(&account))
			When(account.SurplusCoin < hearsay.Coin).BadParam("您的铜板数量不足")
			When(Must2(DB.Where("hearsay_id = ? and user_id = ?", id, UserId(ctx)).Exist(&entities.HearsayUnlock{}))).BadParam("请勿重复解锁")

			data := entities.HearsayUnlock{
				HearsayId:   id,
				UserId:      UserId(ctx),
				Result:      0,
				CreatedTime: time.Now().Unix(),
			}
			Must2(DB.Insert(&data))
			Must2(DB.Insert(&entities.Account{
				Type:          "hearsay-unlock",
				UserId:        from.Id,
				Coin:          -hearsay.Coin,
				Weight:        0,
				SurplusCoin:   account.SurplusCoin - hearsay.Coin,
				SurplusWeight: account.SurplusWeight,
				ReferType:     "hearsay-unlock",
				ReferId:       data.Id,
				Abstract:      utils.CuteString("解锁"+to.Nickname+"发布的:"+hearsay.PublicContent, 40),
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(from.Id).SetExpr("coin", account.SurplusCoin-hearsay.Coin).Update(&from))
			Must2(DB.ID(id).Incr("unlock_count").Update(&hearsay))

			content := []rune(hearsay.PublicContent)
			if len(content) > 20 {
				content = content[:20]
			}
			_ = Send(from.Id, hearsay.CreatorId, fmt.Sprintf("%s解锁了你的传闻:%s", from.Nickname, string(content)), "hearsay", hearsay.Id, true)
			_ = Send(hearsay.CreatorId, from.Id, fmt.Sprintf("你解锁了%s的传闻:%s", to.Nickname, string(content)), "hearsay", hearsay.Id, false)
			return
		}))
		return Ok(ctx)
	})
	e.POST("/hearsay/:id/favor", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			hearsay := entities.Hearsay{}
			Check2(DB.ID(id).Get(&hearsay)).BadParam("传闻不存在")
			unlock := entities.HearsayUnlock{}
			Check2(DB.Where("hearsay_id = ? and user_id = ?", id, UserId(ctx)).Get(&unlock)).BadParam("传闻未解锁")
			When(unlock.SettleTime != 0).BadParam("请勿重复点击")
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			to := entities.User{}
			Check2(DB.ID(hearsay.CreatorId).Get(&to)).BadParam("用户不存在")
			account := entities.Account{}
			Check2(DB.Where("user_id=?", hearsay.CreatorId).Desc("id").Get(&account))

			Must2(DB.Table(&unlock).ID(unlock.Id).Update(map[string]any{
				"result":      1,
				"settle_time": time.Now().Unix(),
			}))
			Must2(DB.Insert(&entities.Account{
				Type:          "hearsay-favor",
				UserId:        hearsay.CreatorId,
				Coin:          hearsay.Coin,
				Weight:        hearsay.Coin,
				SurplusCoin:   account.SurplusCoin + hearsay.Coin,
				SurplusWeight: account.SurplusWeight + hearsay.Coin,
				ReferType:     "hearsay-unlock",
				ReferId:       unlock.Id,
				Abstract:      utils.CuteString(from.Nickname+"打赏:"+hearsay.PublicContent, 40),
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(hearsay.CreatorId).
				SetExpr("coin", account.SurplusCoin+hearsay.Coin).
				SetExpr("weight", account.SurplusWeight+hearsay.Coin).
				Update(&entities.User{}))
			Must2(DB.ID(id).Incr("favor_count").Incr("weight", hearsay.Coin).Update(&hearsay))
			if unlock.RewardId != 0 {
				Must2(DB.ID(unlock.RewardId).Incr("weight", hearsay.Coin).Update(&entities.Reward{}))
			}

			content := []rune(hearsay.PublicContent)
			if len(content) > 20 {
				content = content[:20]
			}
			_ = Send(from.Id, hearsay.CreatorId, fmt.Sprintf("%s给你的传闻点赞:%s", from.Nickname, string(content)), "hearsay", hearsay.Id, true)
			_ = Send(hearsay.CreatorId, from.Id, fmt.Sprintf("你给%s的传闻点赞:%s", to.Nickname, string(content)), "hearsay", hearsay.Id, false)

			ReferreeFavored(DB, to.Id, to.Nickname, to.ReferrerId)
			// 释放被冻结的铜板
			//if unlock.FrozenCoin > 0 {
			//	Must2(DB.ID(unlock.UserId).Incr("coin", unlock.FrozenCoin).Update(&entities.User{}))
			//	_ = Send(hearsay.CreatorId, unlock.UserId, fmt.Sprintf("释放被冻结的铜板:%d", unlock.FrozenCoin), "", 0, true)
			//}
			return
		}))
		return Ok(ctx)
	})
	e.POST("/hearsay/:id/negate", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			hearsay := entities.Hearsay{}
			Check2(DB.ID(id).Get(&hearsay)).BadParam("传闻不存在")
			unlock := entities.HearsayUnlock{}
			Check2(DB.Where("hearsay_id = ? and user_id = ?", id, UserId(ctx)).Get(&unlock)).BadParam("传闻未解锁")
			When(unlock.SettleTime != 0).BadParam("请勿重复点击")
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			to := entities.User{}
			Check2(DB.ID(hearsay.CreatorId).Get(&to)).BadParam("用户不存在")
			account := entities.Account{}
			Check2(DB.Where("user_id=?", hearsay.CreatorId).Desc("id").Get(&account))

			Must2(DB.Table(&unlock).ID(unlock.Id).Update(map[string]any{
				"result":      2,
				"settle_time": time.Now().Unix(),
			}))
			Must2(DB.Insert(&entities.Account{
				Type:          "hearsay-negate",
				UserId:        hearsay.CreatorId,
				Coin:          0,
				Weight:        -hearsay.Coin,
				SurplusCoin:   account.SurplusCoin,
				SurplusWeight: account.SurplusWeight - hearsay.Coin,
				ReferType:     "hearsay-unlock",
				ReferId:       unlock.Id,
				Abstract:      utils.CuteString(to.Nickname+"踩:"+hearsay.PublicContent, 40),
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(hearsay.CreatorId).SetExpr("weight", account.SurplusWeight-hearsay.Coin).Update(&entities.User{}))
			Must2(DB.ID(id).Incr("negate_count").Decr("weight", hearsay.Coin).Update(&hearsay))
			if unlock.RewardId != 0 {
				Must2(DB.ID(unlock.RewardId).Incr("surplus_coin", hearsay.Coin).Decr("weight", hearsay.Coin).Update(&entities.Reward{}))
			}

			content := []rune(hearsay.PublicContent)
			if len(content) > 20 {
				content = content[:20]
			}
			_ = Send(from.Id, hearsay.CreatorId, fmt.Sprintf("%s给你的传闻点踩:%s", from.Nickname, string(content)), "hearsay", hearsay.Id, true)
			_ = Send(hearsay.CreatorId, from.Id, fmt.Sprintf("你给%s的传闻点踩:%s", to.Nickname, string(content)), "hearsay", hearsay.Id, false)

			// 释放被冻结的铜板
			//if unlock.FrozenCoin > 0 {
			//	Must2(DB.ID(unlock.UserId).Incr("coin", unlock.FrozenCoin).Update(&entities.User{}))
			//	_ = Send(hearsay.CreatorId, unlock.UserId, fmt.Sprintf("释放被冻结的铜板:%d", unlock.FrozenCoin), "", 0, true)
			//}
			return
		}))
		return Ok(ctx)
	})
	e.POST("/hearsay/:id/discard", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Table(&entities.HearsayBrowse{}).Where("user_id=? and hearsay_id=?", UserId(ctx), id).Delete())
		return Ok(ctx)
	})
	e.POST("/hearsay-comment", func(ctx echo.Context) error {
		params := Bind(ctx, entities.HearsayComment{})
		When(params.Content == "").BadParam("内容不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			hearsay := entities.Hearsay{}
			Check2(DB.ID(params.TargetId).Get(&hearsay)).BadParam("传闻不存在")
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")

			data := params
			data.CreatorId = from.Id
			data.CreatedTime = time.Now().Unix()
			Must2(DB.Insert(&data))
			Must2(DB.ID(data.TargetId).Incr("comment_count").
				SetExpr("last_comment_user_id", from.Id).SetExpr("last_comment_time", time.Now().Unix()).
				Update(&hearsay))

			_ = Send(from.Id, from.Id, fmt.Sprintf("你发表了评论:%s", params.Content), "hearsay", hearsay.Id, false)
			if hearsay.CreatorId != from.Id {
				_ = Send(from.Id, hearsay.CreatorId, fmt.Sprintf("%s给你的传闻评论:%s", from.Nickname, params.Content), "hearsay", hearsay.Id, true)
			}
			if params.ReplyToId != 0 {
				replyTo := entities.HearsayComment{}
				Check2(DB.ID(params.ReplyToId).Get(&replyTo))
				if replyTo.CreatorId != from.Id && replyTo.CreatorId != hearsay.CreatorId {
					_ = Send(from.Id, replyTo.CreatorId, fmt.Sprintf("%s回复你的评论:%s", from.Nickname, params.Content), "hearsay", hearsay.Id, true)
				}
			}
			_ = Activity(DB, UserId(ctx), "hearsay-comment", data.Id)
			ReferreeActivity(DB, UserId(ctx), from.Nickname, from.ReferrerId)
			return
		}))
		return Ok(ctx)
	})
	e.DELETE("/hearsay-comment/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			data := entities.HearsayComment{}
			Must2(DB.ID(id).Get(&data))
			When(data.CreatorId != UserId(ctx)).Forbidden()
			Must2(DB.ID(id).SetExpr("deleted_time", time.Now().Unix()).Update(&data))
			return
		}))
		return Ok(ctx)
	})
	e.GET("/mine/hearsay", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]entities.Hearsay, 0)
		query := Query(DB).Where("creator_id = ? and blocked = 0 and deleted_time = 0", UserId(ctx))
		if params.Keyword != "" {
			query.Where("public_content like ?", "%"+params.Keyword+"%")
		}
		if params.Sort == "latest" {
			query.Desc("created_time")
		} else {
			query.Desc("weight")
		}
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return NoCacheData(ctx, params.Result(data, count))
	})
	e.GET("/unlocked-hearsay", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]model.HearsayWithUnlock, 0)
		query := Query(DB).Where("h.blocked = 0 and h.deleted_time = 0").
			Where("hu.user_id=?", UserId(ctx))
		if params.Keyword != "" {
			query.Where("h.public_content like ?", "%"+params.Keyword+"%")
		}
		query.Desc("hu.created_time")
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return NoCacheData(ctx, params.Result(data, count))
	})
	e.POST("/reset-hearsay-filter", func(ctx echo.Context) error {
		Must2(DB.Where("user_id = ?", UserId(ctx)).Delete(&entities.HearsayFilter{}))
		return Ok(ctx)
	})

	e.GET("/reward", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		query := Query(DB).Where("u.blocked = 0 and r.blocked = 0 and r.deleted_time = 0")
		if params.Keyword != "" {
			query.Where("u.nickname like ? or r.content like ?", utils.Repeat(any("%"+params.Keyword+"%"), 2)...)
		}
		if params.Sort == "latest" {
			query.Desc("r.created_time")
		} else {
			query.Desc("r.weight")
		}
		data := make([]model.Reward, 0)
		count := Must2(query.Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(data, count))
	})
	e.GET("/reward/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		return Data(ctx, Must2(model.Reward{}.Get(DB, id)))
	})
	e.POST("/reward", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Reward{})
		When(params.Content == "").BadParam("内容不能为空")
		When(params.Coin == 0).BadParam("悬赏的铜板不能为0")
		from := entities.User{}
		Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
		When(from.Blocked).Forbidden("您的账号已被禁用")
		account := entities.Account{}
		Check2(DB.Where("user_id=?", from.Id).Desc("id").Get(&account))
		When(params.Coin > account.SurplusCoin).BadParam("您的铜板不足")

		data := entities.Reward{
			CreatorId:   from.Id,
			CreatedTime: time.Now().Unix(),
			Content:     params.Content,
			Images:      params.Images,
			Coin:        params.Coin,
			SurplusCoin: params.Coin,
			Weight:      params.Coin,
		}
		Must2(DB.Insert(&data))
		Must2(DB.Insert(&entities.Account{
			Type:          "reward",
			UserId:        from.Id,
			Coin:          -params.Coin,
			Weight:        0,
			SurplusCoin:   account.SurplusCoin - params.Coin,
			SurplusWeight: account.SurplusWeight,
			ReferType:     "reward",
			ReferId:       data.Id,
			Abstract:      utils.CuteString(params.Content, 40),
			CreatedTime:   time.Now().Unix(),
		}))
		Must2(DB.ID(from.Id).
			SetExpr("coin", account.SurplusCoin-params.Coin).
			Incr("reward_count").
			Update(&entities.User{}))
		return Data(ctx, data.Id)
	})
	e.DELETE("/reward/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			data := entities.Reward{}
			When(!Must2(DB.ID(id).Get(&data))).NotFound("悬赏不存在")
			When(data.CreatorId != UserId(ctx)).Forbidden()

			Must2(DB.ID(id).SetExpr("deleted_time", time.Now().Unix()).Update(&data))
			return
		}))
		return Ok(ctx)
	})
	// 选择已有的传闻投稿
	e.POST("/reward/:id/submit/:hid", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("悬赏id不能为空")
		hid := Check2(strconv.Atoi(ctx.Param("hid"))).BadParam("传闻id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			reward := entities.Reward{}
			Check2(DB.ID(id).Get(&reward)).BadParam("悬赏不存在")
			hearsay := entities.Hearsay{}
			Check2(DB.ID(hid).Get(&hearsay)).BadParam("传闻不存在")

			data := entities.RewardHearsay{
				RewardId:    id,
				HearsayId:   hid,
				UserId:      UserId(ctx),
				CreatedTime: time.Now().Unix(),
			}
			Must2(DB.Insert(&data))
			Must2(DB.ID(id).Incr("hearsay_count").Update(&reward))

			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			content := []rune(hearsay.PublicContent)
			if len(content) > 20 {
				content = content[:20]
			}
			_ = Send(from.Id, reward.CreatorId, fmt.Sprintf("%s给你的悬赏投稿:%s", from.Nickname, string(content)), "reward", reward.Id, true)
			_ = Activity(DB, UserId(ctx), "reward-submit", data.Id)
			ReferreeActivity(DB, UserId(ctx), from.Nickname, from.ReferrerId)
			return
		}))
		return Ok(ctx)
	})
	// 解锁在悬赏下投稿的传闻
	e.POST("/reward/:id/unlock/:hid", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("悬赏id不能为空")
		hid := Check2(strconv.Atoi(ctx.Param("hid"))).BadParam("传闻id不能为空")
		data := Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			reward := entities.Reward{}
			Check2(DB.ID(id).Get(&reward)).BadParam("悬赏不存在")
			When(reward.CreatorId != UserId(ctx)).BadParam("没有操作权限")
			hearsay := entities.Hearsay{}
			Check2(DB.ID(hid).Get(&hearsay)).BadParam("传闻不存在")
			if reward.SurplusCoin < hearsay.Coin {
				return map[string]any{
					"code":    1,
					"message": "悬赏剩余的铜板不足，是否使用账号中的铜板解锁",
				}, nil
			}
			rewardHearsay := entities.RewardHearsay{}
			Check2(DB.Where("reward_id = ? and hearsay_id = ?", id, hid).Get(&rewardHearsay)).BadParam("关联错误")

			Must2(DB.Insert(&entities.HearsayUnlock{
				HearsayId:   hid,
				UserId:      UserId(ctx),
				RewardId:    id,
				Result:      0,
				CreatedTime: time.Now().Unix(),
			}))
			Must2(DB.ID(id).Decr("surplus_coin", hearsay.Coin).Update(&reward))
			Must2(DB.ID(id).Incr("unlock_count").Update(&hearsay))

			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			to := entities.User{}
			Check2(DB.ID(hearsay.CreatorId).Get(&to)).BadParam("用户不存在")
			content := []rune(hearsay.PublicContent)
			if len(content) > 20 {
				content = content[:20]
			}
			_ = Send(from.Id, hearsay.CreatorId, fmt.Sprintf("%s解锁了你的传闻:%s", from.Nickname, string(content)), "hearsay", hearsay.Id, true)
			_ = Send(hearsay.CreatorId, from.Id, fmt.Sprintf("你解锁了%s的传闻:%s", to.Nickname, string(content)), "hearsay", hearsay.Id, false)
			return
		}))
		return Data(ctx, data)
	})
	e.POST("/reward-comment", func(ctx echo.Context) error {
		params := Bind(ctx, entities.RewardComment{})
		When(params.Content == "").BadParam("内容不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			from := entities.User{}
			Check2(DB.ID(UserId(ctx)).Get(&from)).BadParam("用户不存在")
			reward := entities.Reward{}
			Check2(DB.ID(params.TargetId).Get(&reward)).BadParam("悬赏不存在")

			data := params
			data.CreatorId = from.Id
			data.CreatedTime = time.Now().Unix()
			Must2(DB.Insert(&data))
			Must2(DB.ID(data.TargetId).Incr("comment_count").Update(&reward))

			_ = Send(from.Id, from.Id, fmt.Sprintf("你发表了评论:%s", params.Content), "reward", reward.Id, false)
			if reward.CreatorId != from.Id {
				_ = Send(from.Id, reward.CreatorId, fmt.Sprintf("%s给你的悬赏评论:%s", from.Nickname, params.Content), "reward", reward.Id, true)
			}
			if params.ReplyToId != 0 {
				replyTo := entities.RewardComment{}
				Check2(DB.ID(params.ReplyToId).Get(&replyTo))
				if replyTo.CreatorId != from.Id && replyTo.CreatorId != reward.CreatorId {
					_ = Send(from.Id, replyTo.CreatorId, fmt.Sprintf("%s回复你的评论:%s", from.Nickname, params.Content), "reward", reward.Id, true)
				}
			}
			_ = Activity(DB, UserId(ctx), "reward-comment", data.Id)
			ReferreeActivity(DB, UserId(ctx), from.Nickname, from.ReferrerId)
			return
		}))
		return Ok(ctx)
	})
	e.DELETE("/reward-comment/:id", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			data := entities.RewardComment{}
			Must2(DB.ID(id).Get(&data))
			When(data.CreatorId != UserId(ctx)).Forbidden()
			Must2(DB.ID(id).SetExpr("deleted_time", time.Now().Unix()).Update(&data))
			return
		}))
		return Ok(ctx)
	})

	e.GET("/public/report", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]model.Report, 0)
		count := Must2(Query(DB).Where("r.publicly").Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(data, count))
	})
	e.GET("/mine/report", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]model.ReportWithTargetUser, 0)
		count := Must2(Query(DB).Where("r.creator_id = ?", UserId(ctx)).Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(data, count))
	})
	e.GET("/manager/report", func(ctx echo.Context) error {
		params := Bind(ctx, Paging{})
		data := make([]model.Report, 0)
		count := Must2(Query(DB).Limit(params.Size(), params.Offset()).FindAndCount(&data))
		return Data(ctx, params.Result(data, count))
	})
	e.POST("/report", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Report{})
		When(params.TargetType == "").BadParam("举报类型不能为空")
		When(params.TargetId == 0).BadParam("举报id不能为空")

		targetUserId := 0
		switch params.TargetType {
		case "hearsay":
			Check2(DB.Table(entities.Hearsay{}).ID(params.TargetId).Cols("id").Get(&targetUserId)).BadParam()
		case "hearsay-comment":
			Check2(DB.Table(entities.HearsayComment{}).ID(params.TargetId).Cols("id").Get(&targetUserId)).BadParam()
		}

		data := entities.Report{
			CreatorId:    UserId(ctx),
			CreatedTime:  time.Now().Unix(),
			TargetType:   params.TargetType,
			TargetId:     params.TargetId,
			TargetUserId: targetUserId,
			Content:      params.Content,
			Images:       params.Images,
		}
		Must2(DB.Insert(&data))
		return Data(ctx, data.Id)
	})
	e.POST("/report/:id/reply", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		params := Bind(ctx, entities.Report{})

		Must2(DB.Table(&entities.Report{}).ID(id).Cols("reply", "reply_time", "publicly").Update(map[string]any{
			"reply":      params.Reply,
			"reply_time": time.Now().Unix(),
			"publicly":   params.Publicly,
		}))
		return Ok(ctx)
	})

	// 屏蔽用户、评论、传闻等
	e.POST("/manager/:target/:id/manage", func(ctx echo.Context) error {
		id := Check2(strconv.Atoi(ctx.Param("id"))).BadParam("id不能为空")
		targets := map[string]struct {
			Name   string
			Entity any
		}{
			"person":          {"用户", &entities.User{}},
			"person-comment":  {"评论", &entities.UserComment{}},
			"hearsay":         {"传闻", &entities.Hearsay{}},
			"hearsay-comment": {"评论", &entities.HearsayComment{}},
			"reward":          {"悬赏", &entities.Reward{}},
			"reward-comment":  {"评论", &entities.RewardComment{}},
		}
		target, has := targets[ctx.Param("target")]
		When(!has).BadParam("未定义的目标类型")
		params := Bind(ctx, struct {
			Notice  string `json:"notice"`  // 管理员:提醒
			Blocked bool   `json:"blocked"` // 管理员:已屏蔽
		}{})
		Must2(DB.Transaction(func(DB *xorm.Session) (interface{}, error) {
			When(!Check2(DB.ID(id).Exist(target.Entity)).Internal()).NotFound(target.Name + "不存在")

			Must2(DB.Table(target.Entity).ID(id).Update(map[string]any{
				"notice":  params.Notice,
				"blocked": params.Blocked,
			}))
			return nil, nil
		}))
		return Ok(ctx)
	})
	// 修改动态设置
	e.POST("/manager/config", func(ctx echo.Context) error {
		Config.DynamicConfig = Bind(ctx, DynamicConfig{})
		data := Must2(json.MarshalIndent(Config.DynamicConfig, "", "  "))
		Must(os.WriteFile(WorkDir+"config.json", data, 0666))
		return NoCacheData(ctx, Config.DynamicConfig)
	})
	// 制造铜板
	e.POST("/manager/make-coin", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Account{})
		When(params.UserId == 0).BadParam("用户ID不能为空")
		When(params.Coin < 0).BadParam("铜板数量不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			user := entities.User{}
			Must2(DB.ID(params.UserId).Get(&user))
			lastAccount := entities.Account{}
			Check2(DB.Where("user_id=?", params.UserId).Desc("id").Get(&lastAccount))

			Must2(DB.Insert(&entities.Account{
				Type:          "make-coin",
				UserId:        params.UserId,
				Coin:          params.Coin,
				Weight:        params.Coin,
				SurplusCoin:   lastAccount.SurplusCoin + params.Coin,
				SurplusWeight: lastAccount.SurplusWeight + params.Coin,
				Abstract:      "发行",
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(params.UserId).
				SetExpr("coin", lastAccount.SurplusCoin+params.Coin).
				SetExpr("weight", lastAccount.SurplusWeight+params.Coin).
				Update(&user))
			return
		}))
		return Ok(ctx)
	})
	// 销毁铜板
	e.POST("/manager/destroy-coin", func(ctx echo.Context) error {
		params := Bind(ctx, entities.Account{})
		When(params.UserId == 0).BadParam("用户ID不能为空")
		When(params.Coin < 0).BadParam("铜板数量不能为空")
		Must2(DB.Transaction(func(DB *xorm.Session) (_ interface{}, err error) {
			user := entities.User{}
			Must2(DB.ID(params.UserId).Get(&user))
			lastAccount := entities.Account{}
			Check2(DB.Where("user_id=?", params.UserId).Desc("id").Get(&lastAccount))
			When(lastAccount.SurplusCoin-params.Coin < 0).BadParam("铜板数量不足")
			Must2(DB.Insert(&entities.Account{
				Type:          "destroy-coin",
				UserId:        params.UserId,
				Coin:          -params.Coin,
				Weight:        -params.Coin,
				SurplusCoin:   lastAccount.SurplusCoin - params.Coin,
				SurplusWeight: lastAccount.SurplusWeight - params.Coin,
				Abstract:      "销毁",
				CreatedTime:   time.Now().Unix(),
			}))
			Must2(DB.ID(params.UserId).
				SetExpr("coin", lastAccount.SurplusCoin-params.Coin).
				SetExpr("weight", lastAccount.SurplusWeight-params.Coin).
				Update(&user))
			return
		}))
		return Ok(ctx)
	})
}
